/**
 * Media Source Extension API 扩展了 HTML 规格中的 HTMLMediaElement 标准。
 * 允许 JavaScript 生成用于播放的流媒体。允许 JavaScript 生成流媒体以促成多种场景，比如自适应流、直播流等。
 */

/**
 *    本规格允许 JavaScript 动态的为 audio 和 video 标签构建媒体流。规格定义了一个 MediaSource 对象，
 * 可以作为媒体数据的源为 HTMLMediaElement 提供服务。 MediaSource 对象有一个或多个 SourceBuffer 对象。
 * 应用程序向 SourceBuffer 对象添加数据片段，可以根据系统性能和其他因素调整添加的数据的质量。SourceBuffer 中的数据
 * 作为音视频轨道的缓冲进行管理，并当做文本数据进行解码和播放。
 * 
 *    MediaSource 对象代表了一个 HTMLMediaElement 的媒体数据的源。它跟踪着这个源的 readyState 状态，以及可用于添加媒体数据的 SourceBuffer 对象列表。
 * MediasSource 对象被 web 应用程序创建，然后附加到一个 HTMLMediaElement 上。应用程序使用 sourceBuffers 属性中的 SourceBuffer 对象，
 * 给 SourceBuffer 的源添加媒体数据。HTMLMediaElement 在播放期间如果需要，则可以从 MediaSource 对象中捕获这些媒体数据。
 *    MediaSource 字典：{
 *        sourceBuffers: {SourceBufferList}，只读，包含所有关联的 SourceBuffers 对象。当 readyState 为 'closed' 时这个列表为空，一旦 readyState 变为 ‘open'，就可以使用 addSourceBuffer 函数向该列表添加 SourceBuffer 对象。
 *        activeSourceBuffes: {SourceBufferList}，只读，sourceBuffers 的正在对外提供的成员子集。包括选中的视频轨道、启用的音频轨道以及'show'和'hidden'状态的文本轨道
 *        readyState: {ReadyState}，只读，表明 MediaSource 对象的当前状态，当 MediaSource 对象被创建时必须被设置为 'closed'
 *        duration: {unrestricted double}，允许应用程序来设置展示时长，当 MediaSource 被创建时初始化为 NaN
 *        onsourceopen: {EventHandler}，readyState 变为 open 时触发
 *        onsourceended: {EventHandler}，readyState 变为 ended 时触发
 *        onsourceclose: {EventHandler}，readyState 变为 closed 时触发
 *        addSourceBuffer: {SourceBuffer Function(string type)}，添加一个新的 SourceBuffer 到 sourceBuffers 对象中。
 *        removeSourceBuffer: {void Function(SourceBuffer sourceBuffer)}，从 sourceBuffers 属性中删除指定的 SourceBuffer
 *        endOfStream: {void Function([ EndOfStreamError error])}，媒体流结束信号
 *        setLiveSeekableRange: {void Function(double start, double end)}，修改 live seekable range 变量。这个变量是 HTMLMediaElement Extensions 规格定在 MediaSource 对象上，用于改变 HTMLMediaElement.seekable 行为
 *        clearLiveSeekableRange: {void Function()}，如果 readyState 属性是 'open'，且 live seekable range 变量包含一个 range，那么将该变量设置为一个空 timeRange 对象
 *        isTypeSupported: {boolean Function(string type)}，静态方法，检查 MediaSource 对象是否有能力创建指定 MIME 类型的 SourceBuffer 对象
 *    }
 * 涉及到几个独立类型和枚举类型
 *    enum ReadyState 字典{
 *        closed: 表示这个源当前未添加到任何媒体元素
 *        open: 表示这个源已经被媒体元素打开，并且已准备好要添加到 SourceBuffer 中的数据。
 *        ended: 表示源依然附加在媒体元素上，但 endOfStream 已经调用。
 *    }
 *    SourceBufferList 字典：{
 *        length: {unsigned long}，只读，表示该列表中 SourceBuffer 对象的个数
 *        onaddsourcebuffer: {EventHandler}，成功添加新的 SourceBuffer 时触发
 *        onremovesourcebuffer: {EventHandler}，成功删除 SourceBuffer 时触发
 *        SourceBuffer: {getter Function(unsigned long index)}，获取器，以数组形式访问列表，获取指定下标里的 SourceBuffer 对象
 *    }
 * 
 * 
 * SourceBuffer 接口是本规格中另一个重要接口，其内保存着用于音视频播放的媒体数据。
 *    SourceBuffer 字典：{
 *        mode: {AppendMode}，控制怎样处理媒体片段序列。这个属性初始设置是在对象创建后通过 assSourceBuffer() 实现。
 *        updating: {boolean}，只读，用于标识诸如 appendBuffer() 和 remove() 这样的异步流程是否正在进行。这个属性在对象创建时被初始化为 false
 *        buffered: {TimeRange}，只读，表示缓冲在 SourceBuffer 中的时间范谁对象，这个属性在对象创建时被初始化为空的时间范围对象。
 *        timestampOffset: {double}，控制这个偏移量应用到后添加到这个 SourceBuffer 对象中的媒体段的时间戳上。初始值为0
 *        aduioTracks, videoTracks, textTracks: {AudioTrackList， VideoTrackLIst， TextTrackList}: 这个 SourceBuffer 对象创建的对应类型的轨道对象列表，对应的轨道对象接口：AudioTrack/VideoTrack/TextTrack，都有一个 sourceBuffer 对象，返回创建这个轨道的 SourceBuffer 对象。
 *        appendWindowStart: {double}，追加窗口开始时的展示时间戳，初始值为展示开始时间
 *        appendWindowEnd: {unrestricted double}，追加窗口的结束的展示时间戳，初始值为 Infinity
 *        onupdatestart: {EventHandler}，updating 属性从 false 变为 true 时触发
 *        onupdate: {EventHandler}, 追加或删除操作成功完成时触发，此时 updating 属性从 true 变为 false
 *        onupdateend: {EventHandler}，追加或删除哦操作结束时触发
 *        onerror: {EventHandler}，追加期间出现了错误时触发，此时 updating 从 true 变为 false
 *        onabort: {EventHandler}，通过调用 abort() 终止追加或删除操作时触发，此时 updating 从 true 变为 false
 *        appendBuffer: {void Function(BufferSource data)}，将一个 BufferSource 对象中的数据片段添加到这个 SourceBuffer 对象中
 *        abort: {void Function()}，终止当前分片，并重置片段解析器
 *        remove: {void Function(double start, unestricted double end)}，删除指定时间范围内的媒体数据
 *    }
 * 涉及到接独立类型和枚举类型
 *    enum AppendMode 字典：{
 *        segments: 根据媒体片段的时间戳决定编码帧被放入的位置，媒体片段可以以任意顺序添加
 *        sequence: 媒体段将被视为与媒体段中的时间戳无关的相邻时间段。新媒体段的编码帧会被立刻放到前一个媒体片段编码帧的后面。如果需要产生与前一个媒体段相邻的新媒体段，timestampOffset 属性会被设置成新的偏移量。sequence 模式下插设置 timestampOffset 允许将媒体段插入到时间线中指定的位置而不需要知道任何媒体段的时间戳信息。
 *    }
 * 
 * 
 * URL Object Extensions 定义了 URL 对象上的 createObjectURL 方法
 * URL.createObjectURL(MediaSource mediaSource)，为这个媒体源创建 url 链接。
 */

/**
 * 下面是一个简单示例
 */

/**
 * 模拟视频媒体数据切片
 */
const getInitializationSegment = () => {
  return new ArrayBuffer(1024);
}

/**
 * 从指定其实位置开始获取分片数据
 * @param {double} startTime video 标签当前播放进度时长
 */
const seekToMediaSegmentAt = (startTime) => {
  // 这个函数的本质不在于更新分片，而是通知后台分片代码将定位器修改到新的位置上
  // 以便在下一次获取分片时，得到正确的数据。
  // 这往往是异步的行为，这里为体现顺序，采用同步写法。
}

/**
 * 判断是否还有未播放的媒体分片
 * @return {boolean}
 */
const haveMoreMediaSegments = () => {
  // 具体的算法不清除，但最终目的是判断是否要有添加的媒体分片没有播放。
  // 如果有则返回 true，否则返回 false
}

/**
 * 顾名思义，从服务端分片器获取下一个分片
 */
const GetNextMediaSegment = () => {
  return new ArrayBuffer(1024);
}

/**
 * mediaSource 的 sourceopen 事件处理函数
 * @param {HTMLVideoELement} videoTag 绑定源的 video 标签
 * @param {Event} e sourceopen 事件对象
 */
const onSourceOpen = (videoTag, e) => {
  let mediaSource = e.target;

  if(mediaSource.sourceBuffers.length > 0) return; // 已经有缓冲的片段，则终止

  let sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');

  // 为 video 元素绑定事件处理
  videoTag.addEventListener('seeking', onSeeking.bind(this, mediaSource));
  videoTag.addEventListener('progress', onProgress.bind(this, mediaSource));

  // 获取初始媒体片段
  let initSegment = getInitializationSegment();

  if(initSegment == null) {
    // 如果没有成功获取到初始切片数据，则应该抛出错误并终止流
    mediaSource.endOfStream('network');
    return;
  }

  // 添加初始切片
  let firstAppendHandler = (e) => {
    let sourceBuffer = e.target;
    sourceBuffer.removeEventListener('updateend', firstAppendHandler);

    // TODO: 继续追加之后的视频切片
  }
  sourceBuffer.addEventListener('updateend', firstAppendHandler);
  sourceBuffer.appendBuffer(initSegment);
}

/**
 * 整合添加新分片逻辑流程
 * @param {MediaSource} mediaSource 媒体源对象
 */
const appendNextMediaSegment = (mediaSource) => {
  if(mediaSource.readyState === 'closed') return; // 未 open

  // 如果流数据已经完了，就发送一个流结束信号
  if(!haveMoreMediaSegments()) {
    mediaSource.endOfStream('runout');
    return;
  }

  // 确保前一个添加的 SourceBuffer 已经添加完毕
  if(mediaSource.sourceBuffers[0].updating) return;

  let mediaSegment = GetNextMediaSegment();

  if(!mediaSegment) {
    // 抛出一个获取错误，并结束媒体流
    mediaSource.endOfStream('network');
    return;
  }

  /**
   * NOTE: 如果 mediaSource.readyState === 'ended'，这里 appendBuffer() 的调用会让 readyState 变为 'open'
   * 因此，应用程序应该做好处理多个 'sourceopen' 事件的准备
   */
  mediaSource.sourceBuffers[0].appendBuffer(mediaSegment);
}

/**
 * video 标签 seeking 事件处理函数
 * @param {MediaSource} mediaSource 与 video 标签关联的 medaiSource 对象
 * @param {Evnet } e 事件对象
 */
const onSeeking = (mediaSource, e) => {
  let video = e.target;

  if(mediaSource.readyState === 'open') {
    // 终止当前附加的数据片段
    mediaSource.sourceBuffers[0].abort();
  }

  // 通知媒体片段加载代码在新的播放位置开始获取数据
  seekToMediaSegmentAt(video.currentTime);

  // 添加新位置下的分片数据
  appendNextMediaSegment(mediaSource);
}

/**
 * video 标签 progress 事件处理函数
 * @param {MediaSource} mediaSource 与 video 关联的 mediaSource 对象
 * @param {Event} e 
 */
const onProgress = (mediaSource, e) => {
  appendNextMediaSegment(mediaSource);
}

let video = document.querySelector("#curVideo");
let mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', onSourceOpen.bind(this, video));
video.src = window.URL.createObjectURL(mediaSource);